package com.chen.cloudpicture.manager.websocket;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.chen.cloudpicture.manager.websocket.disruptor.PictureEditEventProducer;
import com.chen.cloudpicture.manager.websocket.model.PictureEditActionEnum;
import com.chen.cloudpicture.manager.websocket.model.PictureEditMessageTypeEnum;
import com.chen.cloudpicture.manager.websocket.model.PictureEditRequestMessage;
import com.chen.cloudpicture.manager.websocket.model.PictureEditResponseMessage;
import com.chen.cloudpicture.model.entity.User;
import com.chen.cloudpicture.model.vo.UserVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 图片编辑 WebSocket 处理器（事件分发器）
 *
 * 每种类؜型的消息处理都可以封装为独立的 Handler 处理器类，也就是采用策略模式。
 * 但是我们这里每种处理都不复杂，就都写到一起了，逻辑再复杂一些可以抽离出来。设计模式应该按需使用。
 * 策略模式：定义一个接口，然后多个实现类实现这个接口，再定义一个工厂初始化加载这些实现类，可以用一个 Map 存储实现类，用到哪个直接 get()就行。
 * （拓展：如果要支持分布式؜ WebSocket，只需要保证要编辑同一图片的用户连接的是相同的服务器即可，和游戏分服务器大区、聊天室分房间都是类似的原理。）
 */
@Component
@Slf4j
public class PictureEditHandler extends TextWebSocketHandler {

    // 正在编辑图片的用户id，key: pictureId, value: 当前正在编辑的用户id
    private final Map<Long, Long> pictureEditingUsers = new ConcurrentHashMap<>();

    // 参与编辑图片的所有会话，key: pictureId, value: 用户会话集合
    private final Map<Long, Set<WebSocketSession>> pictureSessions = new ConcurrentHashMap<>();

    // 引入 disruptor 队列来优化
    @Resource
    @Lazy
    private PictureEditEventProducer pictureEditEventProducer;

    /**
     * 连接建立成功
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
        // 保存会话到集合中
        User user = (User) session.getAttributes().get("user");
        Long pictureId = (Long) session.getAttributes().get("pictureId");
        pictureSessions.putIfAbsent(pictureId, ConcurrentHashMap.newKeySet()); // 第一次添加这个 pictureId 时要先初始化集合
        pictureSessions.get(pictureId).add(session);
        // 构造响应，发送加入编辑的消息通知
        PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
        pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
        String message = String.format("用户 %s 加入编辑", user.getUserName());
        pictureEditResponseMessage.setMessage(message);
        pictureEditResponseMessage.setUser(BeanUtil.copyProperties(user, UserVO.class));
        // 广播给所有用户
        broadcastByPictureId(pictureId, pictureEditResponseMessage);

    }

    /**
     * 收到前端消息，根据事件类别处理消息
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
        // 获取消息内容，将 JSON 转换为 PictureEditRequestMessage
        PictureEditRequestMessage pictureEditRequestMessage = JSONUtil.toBean(message.getPayload(), PictureEditRequestMessage.class);
        /*String type = pictureEditRequestMessage.getType();
        PictureEditMessageTypeEnum pictureEditMessageTypeEnum = PictureEditMessageTypeEnum.getEnumByValue(type);*/
        // 从 session 属性中获取到公共参数
        User user = (User) session.getAttributes().get("user");
        Long pictureId = (Long) session.getAttributes().get("pictureId");
        // 根据事件类型做不同处理
        /*switch (pictureEditMessageTypeEnum) { // INFO 和 ERROR 不需考虑因为这两种类型应该是后端主动返回给前端的，前端不会主动发送这两种类型来分发处理
            case ENTER_EDIT:
                handleEnterEditMessage(pictureEditRequestMessage, session, user, pictureId);
                break;
            case EXIT_EDIT:
                handleExitEditMessage(pictureEditRequestMessage, session, user, pictureId);
                break;
            case EDIT_ACTION:
                handleEditActionMessage(pictureEditRequestMessage, session, user, pictureId);
                break;
            default:
                // 其他消息类型，只给生产者发送错误提示
                PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
                pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ERROR.getValue());
                pictureEditResponseMessage.setMessage("消息类型错误");
                pictureEditResponseMessage.setUser(BeanUtil.copyProperties(user, UserVO.class));
                session.sendMessage(new TextMessage(JSONUtil.toJsonStr(pictureEditResponseMessage)));
                break;
        }*/
        // 生产消息到 disruptor 队列（使用 disruptor 异步处理仅在这里引入，其他地方没有侵入，仍为原生 WebSocket）
        pictureEditEventProducer.publishEvent(pictureEditRequestMessage, session, user, pictureId);
    }

    /**
     * 进入编辑状态事件类型处理
     */
    public void handleEnterEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
        //没有用户正在编辑该图片，才能进入编辑状态
        if (!pictureEditingUsers.containsKey(pictureId)) {
            // 设置用户正在编辑该图片
            pictureEditingUsers.put(pictureId, user.getId());
            // 构造响应，发送加入编辑的消息通知
            PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
            pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ENTER_EDIT.getValue());
            String message = String.format("用户 %s 开始编辑图片", user.getUserName());
            pictureEditResponseMessage.setMessage(message);
            pictureEditResponseMessage.setUser(BeanUtil.copyProperties(user, UserVO.class));
            // 广播给所有用户
            broadcastByPictureId(pictureId, pictureEditResponseMessage);
        }
    }

    /**
     * 退出编辑事件类型处理
     */
    public void handleExitEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
        // 正在编辑的用户
        Long editingUserId = pictureEditingUsers.get(pictureId);
        // 确认是当前的编辑者
        if (editingUserId != null && editingUserId.equals(user.getId())) {
            // 移除用户正在编辑该照片
            pictureEditingUsers.remove(pictureId);
            // 构造响应，发送退出编辑的消息通知
            PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
            pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EXIT_EDIT.getValue());
            String message = String.format("用户 %s 退出编辑图片", user.getUserName());
            pictureEditResponseMessage.setMessage(message);
            pictureEditResponseMessage.setUser(BeanUtil.copyProperties(user, UserVO.class));
            // 广播给所有用户
            broadcastByPictureId(pictureId, pictureEditResponseMessage);
        }
    }

    /**
     * 编辑操作事件类型处理
     */
    public void handleEditActionMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
        // 正在编辑的用户
        Long editingUserId = pictureEditingUsers.get(pictureId);
        String editAction = pictureEditRequestMessage.getEditAction();
        PictureEditActionEnum actionEnum = PictureEditActionEnum.getEnumByValue(editAction);
        if (actionEnum == null) {
            log.error("无效的编辑动作");
            return;
        }
        // 确认是当前的编辑者
        if (editingUserId != null && editingUserId.equals(user.getId())) {
            // 构造响应，发送编辑操作的消息通知
            PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
            pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EDIT_ACTION.getValue());
            String message = String.format("用户%s 执行 %s", user.getUserName(), actionEnum.getText());
            pictureEditResponseMessage.setMessage(message);
            pictureEditResponseMessage.setEditAction(actionEnum.getValue());
            pictureEditResponseMessage.setUser(BeanUtil.copyProperties(user, UserVO.class));
            // 广播给除生产者外的所有用户，否则会造成重复编辑
            broadcastByPictureId(pictureId, pictureEditResponseMessage, session);
        }
    }


    /**
     * 关闭连接
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
        // 从 session 属性中获取到公共参数
        User user = (User) session.getAttributes().get("user");
        Long pictureId = (Long) session.getAttributes().get("pictureId");
        // 移除当前用户的编辑状态
        handleEditActionMessage(null, session, user, pictureId);
        // 删除会话
        Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
        if (sessionSet != null) {
            sessionSet.remove(session);
            if (sessionSet.isEmpty())
                pictureSessions.remove(pictureId);
        }
        // 构造响应，通知其他用户，该用户已经离开编辑
        PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
        pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
        String message = String.format("用户 %s 离开编辑", user.getUserName());
        pictureEditResponseMessage.setMessage(message);
        pictureEditResponseMessage.setUser(BeanUtil.copyProperties(user, UserVO.class));
        // 广播给所有用户
        broadcastByPictureId(pictureId, pictureEditResponseMessage);
    }

    /**
     * 广播给该图片的所有用户（排除自身或其他某个用户session）
     *
     * @param pictureId                  编辑的图片id
     * @param pictureEditResponseMessage 待广播的响应信息
     * @param excludeSession             // 广播操作要排除自身 (自身的session)
     * @throws IOException
     */
    private void broadcastByPictureId(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage, WebSocketSession excludeSession) throws IOException {
        Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
        if (CollUtil.isNotEmpty(sessionSet)) {
            // 样板代码，将 Long 类型转为 String，解决丢失精度问题
            ObjectMapper objectMapper = new ObjectMapper();
            SimpleModule module = new SimpleModule();
            module.addSerializer(Long.class, ToStringSerializer.instance);
            module.addSerializer(Long.TYPE, ToStringSerializer.instance);
            objectMapper.registerModule(module);
            // 序列化为 JSON 字符串
            // String str = JSONUtil.toJsonStr(pictureEditResponseMessage);
            String str = objectMapper.writeValueAsString(pictureEditResponseMessage);
            // 广播
            TextMessage textMessage = new TextMessage(str);
            for (WebSocketSession session : sessionSet) {
                // 排除自身（如果业务更改也可能是排除其他session）
                if (excludeSession != null && session.equals(excludeSession))
                    continue;
                if (session.isOpen())
                    session.sendMessage(textMessage);
            }
        }
    }

    private void broadcastByPictureId(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage) throws IOException {
        broadcastByPictureId(pictureId, pictureEditResponseMessage, null);
    }

}
